/*** Eclipse Class Decompiler plugin, copyright (c) 2016 Chen Chao (cnfree2000@hotmail.com) ***/
package com.google.code.shardbatis.plugin;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;

import com.google.code.shardbatis.builder.ShardConfigHolder;
import com.google.code.shardbatis.builder.ShardConfigParser;
import com.google.code.shardbatis.strategy.ShardSql;
import com.google.code.shardbatis.strategy.ShardStrategy;
import com.google.code.shardbatis.util.ReflectionUtils;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;

@Intercepts({ @org.apache.ibatis.plugin.Signature(type = StatementHandler.class, method = "prepare", args = {
		java.sql.Connection.class }) })
public class ShardPlugin implements Interceptor {
	private int index=0;
	private static final Log log = LogFactory.getLog(ShardPlugin.class);
	public static final String SHARDING_CONFIG = "shardingConfig";
	private static final ConcurrentHashMap<String, Boolean> cache = new ConcurrentHashMap<String, Boolean>();
	public Object intercept(Invocation invocation) throws Throwable {
		String database=((Connection)invocation.getArgs()[0]).getCatalog().toLowerCase();
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		MappedStatement mappedStatement = null;
		if (statementHandler instanceof RoutingStatementHandler) {
			StatementHandler delegate = (StatementHandler) ReflectionUtils.getFieldValue(statementHandler, "delegate");
			mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(delegate, "mappedStatement");
		} else {
			mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(statementHandler, "mappedStatement");
		}

		String mapperId = mappedStatement.getId();
		if (isShouldParse(mapperId)) {
			String sql = statementHandler.getBoundSql().getSql();
			Object params = statementHandler.getBoundSql().getParameterObject();
			log.debug("Old Sql [" + mapperId + "]:" + sql);
			sql = convert(sql, mapperId, params, database);
			log.debug("Converted Sql [" + mapperId + "]:" + sql);
			ReflectionUtils.setFieldValue(statementHandler.getBoundSql(), "sql", sql);
		}
		return invocation.proceed();
	}

	/**
	 * @param oldSql 原始sql语句
	 * @param mapperId mybatis映射id
	 * @param params 参数
	 * @param database 数据库名称
	 * @return
	 * @throws JSQLParserException
	 */
	private String convert(String oldSql, String mapperId, Object params, String database) throws JSQLParserException {
		//配置根据mapper Id处理 sql 优先处理
		ShardSql mappgerStrategy = ShardConfigHolder.getInstance().getMappgerStrategy( mapperId.trim()  );
		if (mappgerStrategy !=null){
			return mappgerStrategy.getTargetSql(oldSql, params, mapperId, database);
		}
		//正常的根据jsqlparser处理
		CCJSqlParserManager pm=new CCJSqlParserManager();
		Statement statement = pm.parse(new StringReader(oldSql));
		if ( statement instanceof Select  ) {
			((Select)statement).getSelectBody().accept(new TableNameModifier(params, mapperId, database));
		}else if(statement instanceof Update){
			Update update = (Update) statement;
			List<Table> tables = update.getTables();
			for (Table table : tables) {
				table.setName(convertTableName(table.getName(), params, mapperId, database) );
			}
		}else if(statement instanceof Insert){
			Insert insert = (Insert) statement;
			String name = insert.getTable().getName();
			insert.getTable().setName(convertTableName(name, params, mapperId, database));
		}else if(statement instanceof Delete){
			Delete delete = (Delete) statement;
			String name = delete.getTable().getName();
			delete.getTable().setName(convertTableName(name, params, mapperId, database));
		} else{
			throw new IllegalArgumentException("The argument statement must is instance of Delete,select,update,insert");
		}
		StatementDeParser deParser = new StatementDeParser(new StringBuilder());
		statement.accept(deParser);
		oldSql= deParser.getBuffer().toString();
		return oldSql;
	}

	/**
	 * @param baseTableName 表名
	 * @param params 参数 
	 * @param mapperId  mybatis 配置 id
	 * @param database TODO
	 * @return
	 */
	private String convertTableName(String baseTableName, Object params, String mapperId, String database) {
		ShardStrategy ss = ShardConfigHolder.getInstance().getStrategy(baseTableName);
		if (ss==null){
			return baseTableName;
		}else{
			return	ss.getTargetTableName(baseTableName, params, mapperId, database);
		}
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {
		String config = properties.getProperty("shardingConfig", null);
		if ((config == null) || (config.trim().length() == 0)) {
			throw new IllegalArgumentException("property 'shardingConfig' is requested.");
		}

		ShardConfigParser parser = new ShardConfigParser();
		InputStream input = null;
		try {
			input = Resources.getResourceAsStream(config);
			parser.parse(input);
		} catch (IOException e) {
		} catch (Exception e) {
		} finally {
			if (input != null)
				try {
					input.close();
				} catch (IOException e) {
					log.error(e.getMessage(), e);
				}
		}
	}

	private boolean isShouldParse(String mapperId) {
		Boolean parse = (Boolean) cache.get(mapperId);

		if (parse != null) {
			return parse.booleanValue();
		}

		if (!(mapperId.endsWith("!selectKey"))) {
			ShardConfigHolder configHolder = ShardConfigHolder.getInstance();

			if ((!(configHolder.isIgnoreId(mapperId)))
					&& (((!(configHolder.isConfigParseId())) || (configHolder.isParseId(mapperId))))) {
				parse = Boolean.valueOf(true);
			}
		}
		if (parse == null) {
			parse = Boolean.valueOf(false);
		}
		cache.put(mapperId, parse);
		return parse.booleanValue();
	}
	private class TableNameModifier extends   SelectDeParser {
		private Object params;
		private String mapperId;
		private String database;
		public TableNameModifier(Object params, String mapperId, String database) {
			this.params = params;
			this.mapperId = mapperId;
			this.database = database;
			setBuffer( new StringBuilder());
		}
		@Override
		public void visit(Table table) {
			if (table.getAlias()==null){
				table.setAlias( new Alias(  table.getName()+(index++)));
			}
			table.setName( convertTableName( table.getName(), params, mapperId, database)  );
		}
		
	}
}
